package com.fw.exception;

import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;

import freemarker.core.Environment;

public class TemplateException extends Exception {

	private static final long serialVersionUID = -6445780084732338268L;

	private static final boolean BEFORE_1_4 = before14();

	@SuppressWarnings({ "rawtypes", "unchecked" })
	private static boolean before14() {
		Class ec = Exception.class;
		try {
			ec.getMethod("getCause", new Class[] {});
		} catch (Throwable e) {
			return true;
		}
		return false;
	}

	@SuppressWarnings("rawtypes")
	private static final Class[] EMPTY_CLASS_ARRAY = new Class[] {};

	private static final Object[] EMPTY_OBJECT_ARRAY = new Object[] {};

	/** The underlying cause of this exception, if any */
	private final Exception causeException;
	private final transient Environment env;
	private final String ftlInstructionStack;

	/**
	 * Constructs a TemplateException with no specified detail message or
	 * underlying cause.
	 */
	public TemplateException(Environment env) {
		this(null, null, env);
	}

	/**
	 * Constructs a TemplateException with the given detail message, but no
	 * underlying cause exception.
	 * 
	 * @param description
	 *            the description of the error that occurred
	 */
	public TemplateException(String description, Environment env) {
		this(description, null, env);
	}

	/**
	 * Constructs a TemplateException with the given underlying Exception, but
	 * no detail message.
	 * 
	 * @param cause
	 *            the underlying <code>Exception</code> that caused this
	 *            exception to be raised
	 */
	public TemplateException(Exception cause, Environment env) {
		this(null, cause, env);
	}

	/**
	 * Constructs a TemplateException with both a description of the error that
	 * occurred and the underlying Exception that caused this exception to be
	 * raised.
	 * 
	 * @param description
	 *            the description of the error that occurred
	 * @param cause
	 *            the underlying <code>Exception</code> that caused this
	 *            exception to be raised
	 */
	public TemplateException(String description, Exception cause, Environment env) {
		super(getDescription(description, cause));
		causeException = cause;
		this.env = env;
		if (env != null) {
			StringWriter sw = new StringWriter();
			PrintWriter pw = new PrintWriter(sw);
			env.outputInstructionStack(pw);
			pw.flush();
			ftlInstructionStack = sw.toString();
		} else {
			ftlInstructionStack = "";
		}
	}

	private static String getDescription(String description, Exception cause) {
		if (description != null) {
			return description;
		}
		if (cause != null) {
			return cause.getClass().getName() + ": " + cause.getMessage();
		}
		return "No error message";
	}

	/**
	 * <p>
	 * Returns the underlying exception that caused this exception to be
	 * generated.
	 * </p>
	 * <p>
	 * <b>Note:</b><br />
	 * avoided calling it <code>getCause</code> to avoid name clash with JDK 1.4
	 * method. This would be problematic because the JDK 1.4 method returns a
	 * <code>Throwable</code> rather than an <code>Exception</code>.
	 * </p>
	 * 
	 * @return the underlying <code>Exception</code>, if any, that caused this
	 *         exception to be raised
	 */
	public Exception getCauseException() {
		return causeException;
	}

	/**
	 * Returns the same exception as <code>getCauseException</code>. Provided to
	 * enable full JDK-generated stack traces when running under JDK 1.4.
	 * 
	 * @see Throwable#getCause()
	 * @return the underlying <code>Exception</code>, if any, that caused this
	 *         exception to be raised
	 */
	public Throwable getCause() {
		return causeException;
	}

	/**
	 * Returns the quote of the problematic FTL instruction and the FTL stack
	 * strace.
	 */
	public String getFTLInstructionStack() {
		return ftlInstructionStack;
	}

	/**
	 * @return the execution environment in which the exception occurred.
	 *         <tt>null</tt> if the exception was deserialized.
	 */
	public Environment getEnvironment() {
		return env;
	}

	public void printStackTrace(java.io.PrintStream ps) {
		PrintWriter pw = new PrintWriter(new OutputStreamWriter(ps), true);
		printStackTrace(pw);
		pw.flush();
	}

	public void printStackTrace(PrintWriter pw) {
		pw.println();
		pw.println(getMessage());
		if (ftlInstructionStack != null && ftlInstructionStack.length() != 0) {
			pw.println("The problematic instruction:");
			pw.println(ftlInstructionStack);
		}
		pw.println("Java backtrace for programmers:");
		pw.println("----------");
		super.printStackTrace(pw);
		if (BEFORE_1_4 && causeException != null) {
			pw.println("Underlying cause: ");
			causeException.printStackTrace(pw);
		}

		// Dirty hack to fight with stupid ServletException class whose
		// getCause() method doesn't work properly. Also an aid for pre-J2xE 1.4
		// users.
		try {
			// Reflection is used to prevent dependency on Servlet classes.
			Method m = causeException.getClass().getMethod("getRootCause", EMPTY_CLASS_ARRAY);
			Throwable rootCause = (Throwable) m.invoke(causeException, EMPTY_OBJECT_ARRAY);
			if (rootCause != null) {
				Throwable j14Cause = null;
				if (!BEFORE_1_4) {
					m = causeException.getClass().getMethod("getCause", EMPTY_CLASS_ARRAY);
					j14Cause = (Throwable) m.invoke(causeException, EMPTY_OBJECT_ARRAY);
				}
				if (j14Cause == null) {
					pw.println("ServletException root cause: ");
					rootCause.printStackTrace(pw);
				}
			}
		} catch (Throwable exc) {
			// ignore
		}
	}

}
